import { TransportError } from "../exceptions";
import { C, IncomingRequestMessage } from "../messages";
import { Timers } from "../timers";
import { Transport } from "../transport";
import { ServerTransaction } from "./server-transaction";
import { TransactionState } from "./transaction-state";
import { ServerTransactionUser } from "./transaction-user";

/**
 * INVITE Server Transaction.
 * @remarks
 * https://tools.ietf.org/html/rfc3261#section-17.2.1
 * @public
 */
export class InviteServerTransaction extends ServerTransaction {
  private lastFinalResponse: string | undefined;
  private lastProvisionalResponse: string | undefined;
  private H: number | undefined;
  private I: number | undefined;
  private L: number | undefined;

  /**
   * FIXME: This should not be here. It should be in the UAS.
   *
   * If the UAS desires an extended period of time to answer the INVITE,
   * it will need to ask for an "extension" in order to prevent proxies
   * from canceling the transaction.  A proxy has the option of canceling
   * a transaction when there is a gap of 3 minutes between responses in a
   * transaction.  To prevent cancellation, the UAS MUST send a non-100
   * provisional response at every minute, to handle the possibility of
   * lost provisional responses.
   *
   *   An INVITE transaction can go on for extended durations when the
   *   user is placed on hold, or when interworking with PSTN systems
   *   which allow communications to take place without answering the
   *   call.  The latter is common in Interactive Voice Response (IVR)
   *   systems.
   * https://tools.ietf.org/html/rfc3261#section-13.3.1.1
   */
  private progressExtensionTimer: number | undefined;

  /**
   * Constructor.
   * Upon construction, a "100 Trying" reply will be immediately sent.
   * After construction the transaction will be in the "proceeding" state and the transaction
   * `id` will equal the branch parameter set in the Via header of the incoming request.
   * https://tools.ietf.org/html/rfc3261#section-17.2.1
   * @param request - Incoming INVITE request from the transport.
   * @param transport - The transport.
   * @param user - The transaction user.
   */
  constructor(request: IncomingRequestMessage, transport: Transport, user: ServerTransactionUser) {
    super(request, transport, user, TransactionState.Proceeding, "sip.transaction.ist");
  }

  /**
   * Destructor.
   */
  public dispose(): void {
    this.stopProgressExtensionTimer();
    if (this.H) {
      clearTimeout(this.H);
      this.H = undefined;
    }
    if (this.I) {
      clearTimeout(this.I);
      this.I = undefined;
    }
    if (this.L) {
      clearTimeout(this.L);
      this.L = undefined;
    }
    super.dispose();
  }

  /** Transaction kind. Deprecated. */
  get kind(): string {
    return "ist";
  }

  /**
   * Receive requests from transport matching this transaction.
   * @param request - Request matching this transaction.
   */
  public receiveRequest(request: IncomingRequestMessage): void {
    switch (this.state) {
      case TransactionState.Proceeding:
        // If a request retransmission is received while in the "Proceeding" state, the most
        // recent provisional response that was received from the TU MUST be passed to the
        // transport layer for retransmission.
        // https://tools.ietf.org/html/rfc3261#section-17.2.1
        if (request.method === C.INVITE) {
          if (this.lastProvisionalResponse) {
            this.send(this.lastProvisionalResponse).catch((error: TransportError) => {
              this.logTransportError(error, "Failed to send retransmission of provisional response.");
            });
          }
          return;
        }
        break;
      case TransactionState.Accepted:
        // While in the "Accepted" state, any retransmissions of the INVITE
        // received will match this transaction state machine and will be
        // absorbed by the machine without changing its state. These
        // retransmissions are not passed onto the TU.
        // https://tools.ietf.org/html/rfc6026#section-7.1
        if (request.method === C.INVITE) {
          return;
        }
        break;
      case TransactionState.Completed:
        // Furthermore, while in the "Completed" state, if a request retransmission is
        // received, the server SHOULD pass the response to the transport for retransmission.
        // https://tools.ietf.org/html/rfc3261#section-17.2.1
        if (request.method === C.INVITE) {
          if (!this.lastFinalResponse) {
            throw new Error("Last final response undefined.");
          }
          this.send(this.lastFinalResponse).catch((error: TransportError) => {
            this.logTransportError(error, "Failed to send retransmission of final response.");
          });
          return;
        }
        // If an ACK is received while the server transaction is in the "Completed" state,
        // the server transaction MUST transition to the "Confirmed" state.
        // https://tools.ietf.org/html/rfc3261#section-17.2.1
        if (request.method === C.ACK) {
          this.stateTransition(TransactionState.Confirmed);
          return;
        }
        break;
      case TransactionState.Confirmed:
        // The purpose of the "Confirmed" state is to absorb any additional ACK messages that arrive,
        // triggered from retransmissions of the final response.
        // https://tools.ietf.org/html/rfc3261#section-17.2.1
        if (request.method === C.INVITE || request.method === C.ACK) {
          return;
        }
        break;
      case TransactionState.Terminated:
        // For good measure absorb any additional messages that arrive (should not happen).
        if (request.method === C.INVITE || request.method === C.ACK) {
          return;
        }
        break;
      default:
        throw new Error(`Invalid state ${this.state}`);
    }

    const message = `INVITE server transaction received unexpected ${request.method} request while in state ${this.state}.`;
    this.logger.warn(message);
    return;
  }

  /**
   * Receive responses from TU for this transaction.
   * @param statusCode - Status code of response.
   * @param response - Response.
   */
  public receiveResponse(statusCode: number, response: string): void {
    if (statusCode < 100 || statusCode > 699) {
      throw new Error(`Invalid status code ${statusCode}`);
    }

    switch (this.state) {
      case TransactionState.Proceeding:
        // The TU passes any number of provisional responses to the server
        // transaction. So long as the server transaction is in the
        // "Proceeding" state, each of these MUST be passed to the transport
        // layer for transmission. They are not sent reliably by the
        // transaction layer (they are not retransmitted by it) and do not cause
        // a change in the state of the server transaction.
        // https://tools.ietf.org/html/rfc3261#section-17.2.1
        if (statusCode >= 100 && statusCode <= 199) {
          this.lastProvisionalResponse = response;
          // Start the progress extension timer only for a non-100 provisional response.
          if (statusCode > 100) {
            this.startProgressExtensionTimer(); // FIXME: remove
          }
          this.send(response).catch((error: TransportError) => {
            this.logTransportError(error, "Failed to send 1xx response.");
          });
          return;
        }
        // If, while in the "Proceeding" state, the TU passes a 2xx response
        // to the server transaction, the server transaction MUST pass this
        // response to the transport layer for transmission. It is not
        // retransmitted by the server transaction; retransmissions of 2xx
        // responses are handled by the TU. The server transaction MUST then
        // transition to the "Accepted" state.
        // https://tools.ietf.org/html/rfc6026#section-8.5
        if (statusCode >= 200 && statusCode <= 299) {
          this.lastFinalResponse = response;
          this.stateTransition(TransactionState.Accepted);
          this.send(response).catch((error: TransportError) => {
            this.logTransportError(error, "Failed to send 2xx response.");
          });
          return;
        }
        // While in the "Proceeding" state, if the TU passes a response with
        // status code from 300 to 699 to the server transaction, the response
        // MUST be passed to the transport layer for transmission, and the state
        // machine MUST enter the "Completed" state.
        // https://tools.ietf.org/html/rfc3261#section-17.2.1
        if (statusCode >= 300 && statusCode <= 699) {
          this.lastFinalResponse = response;
          this.stateTransition(TransactionState.Completed);
          this.send(response).catch((error: TransportError) => {
            this.logTransportError(error, "Failed to send non-2xx final response.");
          });
          return;
        }
        break;
      case TransactionState.Accepted:
        // While in the "Accepted" state, if the TU passes a 2xx response,
        // the server transaction MUST pass the response to the transport layer for transmission.
        // https://tools.ietf.org/html/rfc6026#section-8.7
        if (statusCode >= 200 && statusCode <= 299) {
          this.send(response).catch((error: TransportError) => {
            this.logTransportError(error, "Failed to send 2xx response.");
          });
          return;
        }
        break;
      case TransactionState.Completed:
        break;
      case TransactionState.Confirmed:
        break;
      case TransactionState.Terminated:
        break;
      default:
        throw new Error(`Invalid state ${this.state}`);
    }

    const message = `INVITE server transaction received unexpected ${statusCode} response from TU while in state ${this.state}.`;
    this.logger.error(message);
    throw new Error(message);
  }

  /**
   * Retransmit the last 2xx response. This is a noop if not in the "accepted" state.
   */
  public retransmitAcceptedResponse(): void {
    if (this.state === TransactionState.Accepted && this.lastFinalResponse) {
      this.send(this.lastFinalResponse).catch((error: TransportError) => {
        this.logTransportError(error, "Failed to send 2xx response.");
      });
    }
  }

  /**
   * First, the procedures in [4] are followed, which attempt to deliver the response to a backup.
   * If those should all fail, based on the definition of failure in [4], the server transaction SHOULD
   * inform the TU that a failure has occurred, and MUST remain in the current state.
   * https://tools.ietf.org/html/rfc6026#section-8.8
   */
  protected onTransportError(error: Error): void {
    if (this.user.onTransportError) {
      this.user.onTransportError(error);
    }
  }

  /** For logging. */
  protected typeToString(): string {
    return "INVITE server transaction";
  }

  /**
   * Execute a state transition.
   * @param newState - New state.
   */
  private stateTransition(newState: TransactionState): void {
    // Assert valid state transitions.
    const invalidStateTransition = (): void => {
      throw new Error(`Invalid state transition from ${this.state} to ${newState}`);
    };

    switch (newState) {
      case TransactionState.Proceeding:
        invalidStateTransition();
        break;
      case TransactionState.Accepted:
      case TransactionState.Completed:
        if (this.state !== TransactionState.Proceeding) {
          invalidStateTransition();
        }
        break;
      case TransactionState.Confirmed:
        if (this.state !== TransactionState.Completed) {
          invalidStateTransition();
        }
        break;
      case TransactionState.Terminated:
        if (
          this.state !== TransactionState.Accepted &&
          this.state !== TransactionState.Completed &&
          this.state !== TransactionState.Confirmed
        ) {
          invalidStateTransition();
        }
        break;
      default:
        invalidStateTransition();
    }

    // On any state transition, stop resending provisional responses
    this.stopProgressExtensionTimer();

    // The purpose of the "Accepted" state is to absorb retransmissions of an accepted INVITE request.
    // Any such retransmissions are absorbed entirely within the server transaction.
    // They are not passed up to the TU since any downstream UAS cores that accepted the request have
    // taken responsibility for reliability and will already retransmit their 2xx responses if necessary.
    // https://tools.ietf.org/html/rfc6026#section-8.7
    if (newState === TransactionState.Accepted) {
      this.L = setTimeout(() => this.timerL(), Timers.TIMER_L);
    }

    // When the "Completed" state is entered, timer H MUST be set to fire in 64*T1 seconds for all transports.
    // Timer H determines when the server transaction abandons retransmitting the response.
    // If an ACK is received while the server transaction is in the "Completed" state,
    // the server transaction MUST transition to the "Confirmed" state.
    // https://tools.ietf.org/html/rfc3261#section-17.2.1
    if (newState === TransactionState.Completed) {
      // FIXME: Missing timer G for unreliable transports.
      this.H = setTimeout(() => this.timerH(), Timers.TIMER_H);
    }

    // The purpose of the "Confirmed" state is to absorb any additional ACK messages that arrive,
    // triggered from retransmissions of the final response. When this state is entered, timer I
    // is set to fire in T4 seconds for unreliable transports, and zero seconds for reliable
    // transports. Once timer I fires, the server MUST transition to the "Terminated" state.
    // https://tools.ietf.org/html/rfc3261#section-17.2.1
    if (newState === TransactionState.Confirmed) {
      // FIXME: This timer is not getting set correctly for unreliable transports.
      this.I = setTimeout(() => this.timerI(), Timers.TIMER_I);
    }

    // Once the transaction is in the "Terminated" state, it MUST be destroyed immediately.
    // https://tools.ietf.org/html/rfc6026#section-8.7
    if (newState === TransactionState.Terminated) {
      this.dispose();
    }

    // Update state.
    this.setState(newState);
  }

  /**
   * FIXME: UAS Provisional Retransmission Timer. See RFC 3261 Section 13.3.1.1
   * This is in the wrong place. This is not a transaction level thing. It's a UAS level thing.
   */
  private startProgressExtensionTimer(): void {
    // Start the progress extension timer only for the first non-100 provisional response.
    if (this.progressExtensionTimer === undefined) {
      this.progressExtensionTimer = setInterval(() => {
        this.logger.debug(`Progress extension timer expired for INVITE server transaction ${this.id}.`);
        if (!this.lastProvisionalResponse) {
          throw new Error("Last provisional response undefined.");
        }
        this.send(this.lastProvisionalResponse).catch((error: TransportError) => {
          this.logTransportError(error, "Failed to send retransmission of provisional response.");
        });
      }, Timers.PROVISIONAL_RESPONSE_INTERVAL);
    }
  }

  /**
   * FIXME: UAS Provisional Retransmission Timer id. See RFC 3261 Section 13.3.1.1
   * This is in the wrong place. This is not a transaction level thing. It's a UAS level thing.
   */
  private stopProgressExtensionTimer(): void {
    if (this.progressExtensionTimer !== undefined) {
      clearInterval(this.progressExtensionTimer);
      this.progressExtensionTimer = undefined;
    }
  }

  /**
   * While in the "Proceeding" state, if the TU passes a response with status code
   * from 300 to 699 to the server transaction, the response MUST be passed to the
   * transport layer for transmission, and the state machine MUST enter the "Completed" state.
   * For unreliable transports, timer G is set to fire in T1 seconds, and is not set to fire for
   * reliable transports. If timer G fires, the response is passed to the transport layer once
   * more for retransmission, and timer G is set to fire in MIN(2*T1, T2) seconds. From then on,
   * when timer G fires, the response is passed to the transport again for transmission, and
   * timer G is reset with a value that doubles, unless that value exceeds T2, in which case
   * it is reset with the value of T2.
   * https://tools.ietf.org/html/rfc3261#section-17.2.1
   */
  private timerG(): void {
    // TODO
  }

  /**
   * If timer H fires while in the "Completed" state, it implies that the ACK was never received.
   * In this case, the server transaction MUST transition to the "Terminated" state, and MUST
   * indicate to the TU that a transaction failure has occurred.
   * https://tools.ietf.org/html/rfc3261#section-17.2.1
   */
  private timerH(): void {
    this.logger.debug(`Timer H expired for INVITE server transaction ${this.id}.`);
    if (this.state === TransactionState.Completed) {
      this.logger.warn("ACK to negative final response was never received, terminating transaction.");
      this.stateTransition(TransactionState.Terminated);
    }
  }

  /**
   * Once timer I fires, the server MUST transition to the "Terminated" state.
   * https://tools.ietf.org/html/rfc3261#section-17.2.1
   */
  private timerI(): void {
    this.logger.debug(`Timer I expired for INVITE server transaction ${this.id}.`);
    this.stateTransition(TransactionState.Terminated);
  }

  /**
   * When Timer L fires and the state machine is in the "Accepted" state, the machine MUST
   * transition to the "Terminated" state. Once the transaction is in the "Terminated" state,
   * it MUST be destroyed immediately. Timer L reflects the amount of time the server
   * transaction could receive 2xx responses for retransmission from the
   * TU while it is waiting to receive an ACK.
   * https://tools.ietf.org/html/rfc6026#section-7.1
   * https://tools.ietf.org/html/rfc6026#section-8.7
   */
  private timerL(): void {
    this.logger.debug(`Timer L expired for INVITE server transaction ${this.id}.`);
    if (this.state === TransactionState.Accepted) {
      this.stateTransition(TransactionState.Terminated);
    }
  }
}
